Skip to content

Atespace: per-tenant scoping for the actor lifecycle#280

Open
Haven Xia (HavenXia) wants to merge 6 commits into
agent-substrate:mainfrom
HavenXia:atespace-inc1
Open

Atespace: per-tenant scoping for the actor lifecycle#280
Haven Xia (HavenXia) wants to merge 6 commits into
agent-substrate:mainfrom
HavenXia:atespace-inc1

Conversation

@HavenXia

@HavenXia Haven Xia (HavenXia) commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

This is part of solution for #21.

First incremental slice of the atespace design for actors — part of #21. An atespace is a mandatory tenant boundary that every actor belongs to. It's folded into the actor's identity and storage key (actor:<atespace>:<id>), so list/get/delete within a tenant is a cheap key-prefix operation, and actors in different atespaces can reuse the same id without colliding.

This PR adds atespace through the actor lifecycle end-to-end (proto → store → control API → kubectl-ate).

It doesn't touch DNS, snapshots, scheduling, or auth. Landing it on its own so the future changes are additive, and everything that isn't actor-CRUD is explicitly out of scope below.

This PR changes:

  • Proto — every actor RPC request carries an atespace; Actor carries it as part of its identity.
  • Store — actors are keyed actor:<atespace>:<id> - GetActor/DeleteActor/ListActors take an atespace. Listing is a scoped SCAN actor:<atespace>:*, or SCAN actor:* for all atespaces.
  • Control API — create / get / delete / suspend / pause / resume / update are all atespace-scoped. atespace is required and validated as a DNS-1123 label. The syncer's dead-worker recovery is atespace-aware by adding Worker.actor_atespace.
  • changes on kubectl-ate:
    • --atespace / -a on every actor subcommand (create/get/delete/resume/suspend/pause/logs).
    • -A / --all-atespaces to list across all tenants.
    • Add an ATESPACE column in the actor table, the existing namespace column is renamed TEMPLATE NS to disambiguate it from the atespace.

Examples

Scope a listing to one tenant (-a is shorthand for --atespace):

Creation

$ kubectl ate create actor test -t ate-demo-counter/counter --atespace team-a
ATESPACE   TEMPLATE NS        TEMPLATE   ID     STATUS             ATEOM POD   ATEOM IP   VERSION
team-a     ate-demo-counter   counter    test   STATUS_SUSPENDED   <none>                 1

$ kubectl ate create actor test2 -t ate-demo-counter/counter --atespace team-a
ATESPACE   TEMPLATE NS        TEMPLATE   ID      STATUS             ATEOM POD   ATEOM IP   VERSION
team-a     ate-demo-counter   counter    test2   STATUS_SUSPENDED   <none>                 1

$ kubectl ate create actor test -t ate-demo-counter/counter --atespace team-b
ATESPACE   TEMPLATE NS        TEMPLATE   ID     STATUS             ATEOM POD   ATEOM IP   VERSION
team-b     ate-demo-counter   counter    test   STATUS_SUSPENDED   <none>                 1

Get by atespaces

$ kubectl ate get actors -A
ATESPACE   TEMPLATE NS        TEMPLATE   ID      STATUS             ATEOM POD   ATEOM IP   VERSION
team-a     ate-demo-counter   counter    test    STATUS_SUSPENDED   <none>                 1
team-a     ate-demo-counter   counter    test2   STATUS_SUSPENDED   <none>                 1
team-b     ate-demo-counter   counter    test    STATUS_SUSPENDED   <none>                 1

$ kubectl ate get actors -a team-a
ATESPACE   TEMPLATE NS        TEMPLATE   ID      STATUS             ATEOM POD   ATEOM IP   VERSION
team-a     ate-demo-counter   counter    test    STATUS_SUSPENDED   <none>                 1
team-a     ate-demo-counter   counter    test2   STATUS_SUSPENDED   <none>                 1

$ kubectl ate get actors -a team-b
ATESPACE   TEMPLATE NS        TEMPLATE   ID     STATUS             ATEOM POD   ATEOM IP   VERSION
team-b     ate-demo-counter   counter    test   STATUS_SUSPENDED   <none>                 1

Resume & Suspend

$ kubectl ate resume actor test -a team-a
ATESPACE   TEMPLATE NS        TEMPLATE   ID     STATUS           ATEOM POD                                              ATEOM IP     VERSION
team-a     ate-demo-counter   counter    test   STATUS_RUNNING   ate-demo-counter/counter-deployment-5d9b77d6fd-r846n   10.52.3.86   3

$ kubectl ate get actors -a team-a
ATESPACE   TEMPLATE NS        TEMPLATE   ID      STATUS             ATEOM POD                                              ATEOM IP     VERSION
team-a     ate-demo-counter   counter    test    STATUS_RUNNING     ate-demo-counter/counter-deployment-5d9b77d6fd-r846n   10.52.3.86   3
team-a     ate-demo-counter   counter    test2   STATUS_SUSPENDED   <none>                                                              1

$ kubectl ate suspend actor test -a team-a
ATESPACE   TEMPLATE NS        TEMPLATE   ID     STATUS             ATEOM POD   ATEOM IP   VERSION
team-a     ate-demo-counter   counter    test   STATUS_SUSPENDED   <none>                 5

Scope / non-goals

Deferred to later atespace increments (intentionally not in this PR): the Atespace object + CRUD
RPCs, DNS names, snapshot paths, template grants, the worker's own (system) atespace, and quota.

  • Tests pass
  • Appropriate documentation

@HavenXia Haven Xia (HavenXia) changed the title Atespace inc1 The initial shape of atespace Jun 19, 2026
@HavenXia Haven Xia (HavenXia) changed the title The initial shape of atespace Atespace: per-tenant scoping for the actor lifecycle Jun 19, 2026
@HavenXia Haven Xia (HavenXia) marked this pull request as ready for review June 19, 2026 19:41

@thockin Tim Hockin (thockin) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently an atespace is created merely by using it in an actor. I think we want to move to Atespaces being explicitly created and managed, right? I'm fine with that as a followup, or as more commits in here.

string name = 1;
}

message GetActorRequest {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just join name and atespace into a message and use it everywhere? It seems like there's never a context where we want one and not the other?


// Atespace is the tenant boundary an Actor is created into. Placeholder for now
// (name only).
message Atespace {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed I would find CRUD APIs for Atespaces, but I don't see them?

// Lists all known actors. Returns a page of actors and a next page token.
ListActors(ctx context.Context, pageSize int32, pageToken string) ([]*ateapipb.Actor, string, error)
// Lists actors in the given atespace (scoped scan). Returns a page of actors and a next page token.
ListActors(ctx context.Context, atespace string, pageSize int32, pageToken string) ([]*ateapipb.Actor, string, error)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does "" mean "all atespaces" ?

Selector worker_selector = 4;

// The atespace to create the actor into.
string atespace = 5;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the caution of no renumbering proto tags, but we should strive to make this as readable as possible, so I would put it next to ID

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, we are still in a phase, we can break API. Just put a comment in PR that this is a breaking change and all actors needs to be deleted.

In any case, all existing actors will not work, since they will miss namespace.

func init() {
createActorCmd.Flags().StringVarP(&templateFlag, "template", "t", "", "Template to derive the actor from in <namespace>/<name> format (required)")
_ = createActorCmd.MarkFlagRequired("template")
createActorCmd.Flags().StringVar(&atespaceFlag, "atespace", "", "Atespace (tenant) to create the actor in (required)")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid the word "tenant"


createReq := &ateapipb.CreateActorRequest{
ActorId: actorID,
Atespace: at.ObjectMeta.Namespace,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not obvious to me -- we should never assume k8s namespaces == atespaces -- they are not the same.

We are taking a snapshot, so we should probably use an atespace that is specifically put aside for this. If we move to CRUD of atespaces as a first-class resource, we should "reserve" any atespace whose name being with "ate-", so this can be something like "ate-golden"?

dnsDomainParts := strings.Split("."+resources.ActorDNSSuffix+".", ".")
dnsDomainRef := strings.Join(dnsDomainParts, `\.`)
directives = append(directives, fmt.Sprintf(` match "^%s%s$"`, resources.ActorIDRegexPattern, dnsDomainRef))
directives = append(directives, fmt.Sprintf(` match "^%s\.%s%s$"`, resources.ActorIDRegexPattern, resources.ActorIDRegexPattern, dnsDomainRef))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fishy -- why do we need an explicit \. in one place but not the other?

Debugger says: the input is ".actors.resources.substrate.ate.dev." (leading and trailing dots). That makes dnsDomainParts have empty first and last entries. That means dnsDomainRef has leading and trailing \.. That makes this correct.

It's WEIRD but correct. I'll send a followup to document it

// "<actor_id>.<atespace>.actors.resources.substrate.ate.dev" (a trailing dot is
// tolerated) into its atespace and actor id, validating both. It does not accept
// a host:port; callers must strip the port first.
func ParseActorDNSName(name string) (atespace, actorID string, err error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a perfect target for a small unit test?

Selector worker_selector = 4;

// The atespace to create the actor into.
string atespace = 5;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, we are still in a phase, we can break API. Just put a comment in PR that this is a breaking change and all actors needs to be deleted.

In any case, all existing actors will not work, since they will miss namespace.

if req.GetAtespace() == "" {
return status.Error(codes.InvalidArgument, "atespace is required")
}
if err := resources.ValidateAtespace(req.GetAtespace()); err != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am curious, why "resources.ValidateAtespace" does not validate for emptry string.
I see similar behavior is defined for resources.ValidateActorID too.
Might be we need to fix all the "resource validator" to check for empty string.

Julian Gutierrez Oschmann (@juli4n) - WDYT?

if req.GetActorId() == "" {
return status.Error(codes.InvalidArgument, "id is required")
}
if req.GetAtespace() == "" {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you considered to have full valudation? resources.ValidateNamespace.

It might worth to add similar full validation for actorID itself.

P.S - please make similar fix for all APIs, if you decide to accept the comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants